package javango.contrib.hibernate;

import java.lang.annotation.Annotation;
import java.util.Iterator;

import javango.contrib.hibernate.HibernateFormHelper;
import javango.contrib.hibernate.HibernateUtil;
import javango.db.Manager;
import javango.db.ManagerException;
import javango.db.Managers;
import javango.forms.fields.Field;
import javango.forms.fields.FieldFactory;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.hibernate.MappingException;
import org.hibernate.cfg.Configuration;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.type.AssociationType;
import org.hibernate.type.CollectionType;

import com.google.inject.Inject;

public class ModelForm<T> extends javango.forms.ModelForm<T> {
	
	private final static Log log = LogFactory.getLog(ModelForm.class);

	protected HibernateUtil hibernateUtil;
	protected Managers managers;
	
	@Inject
	public ModelForm(FieldFactory fieldFactory, HibernateUtil hibernateUtil, Managers managers) {
		super(fieldFactory);
		this.hibernateUtil = hibernateUtil;
		this.managers = managers;
	}

	public T save() throws ManagerException {
		Manager<T> manager = managers.forClass(getModel());
		T object = null;
		if (getInstance() != null) {
			object = (T)clean(instance);
		} else {
			object = (T)clean(getModel());
		}
		manager.save(object);
		return object;
	}
	
	// if non-null this form will save to this instance
	T instance = null;
	
	
	/**
	 * @Deprecated Use setInstance instead 
	 */
	@Override	
	public ModelForm<T> setInitial(Object bean) {		
		super.setInitial(bean);
		if (bean != null && getModel().isAssignableFrom(bean.getClass())) {
			this.instance = (T)bean;
		}
		return this;
	}
		
	public ModelForm<T> setInstance(T instance) {
		this.instance = instance;
		setInitial(instance);
		return this;
	}
	
	public T getInstance() {
		return instance;
	}
	
	@Override
	public boolean isValid() {
		if (!super.isValid()) return false;
		
		// check the validators defined in the form
		if (!HibernateFormHelper.isValid(this)) return false;

		// check the validators defined in the modelClass
		return HibernateFormHelper.isValid(this, getModel());
	}
	
	protected Field getFieldType(Property property, java.lang.reflect.Field field) {
		if (property.getType() instanceof CollectionType) {
			// TODO Need to figure out correct way to may collections / multi way collections.
			return null;
//			AssociationType assType = (AssociationType)property.getType();
//			String c = assType.getAssociatedEntityName((SessionFactoryImplementor)HibernateUtil.getSessionFactory());
//			try {
//				return new ModelMultipleChoiceField(Class.forName(c)).setRequired(false); // TODO how to better control this..  which come first the chicken or the egg?
//			} catch (ClassNotFoundException e) {
//				log.error(e,e);
//				return null;
//			}
		} else if (property.getType() instanceof AssociationType) {
			AssociationType assType = (AssociationType)property.getType();
			String c = assType.getAssociatedEntityName((SessionFactoryImplementor)hibernateUtil.getSessionFactory());
			try {
				return new ModelChoiceField(fieldFactory.getWidgetFactory(), hibernateUtil, managers).setModel(Class.forName(c));
			} catch (ClassNotFoundException e) {
				log.error(e,e);
				return null;
			}
		} else {
			return fieldFactory.newFieldForClass(field.getType());
		}

	}
	
	protected java.lang.reflect.Field findField(Class modelClass, Property property) throws NoSuchFieldException {
		Class classToTry = modelClass;
		while(classToTry != null) {
			try {
				return classToTry.getDeclaredField(property.getName());
			} catch (NoSuchFieldException e) {
				classToTry = classToTry.getSuperclass();
			}
		}
		return modelClass.getDeclaredField(property.getName()); // let this throw the excpetion,  i guess we could do so ourselves...
	}
	
	protected Field processField(Property property, String fieldName) {
		if (log.isDebugEnabled()) log.debug("Processing Field :" + fieldName);
		
		// The field if any that comes from the model class
		java.lang.reflect.Field modelField = null;
		
		// The field if any that comes from the form class
		java.lang.reflect.Field formField = null;
		
		// The actual javango Form Field,  this is what we really want to find.
		Field<?> field = null;
		
		try {
			// first see if this modelform has overridden the field...
			formField = this.getClass().getDeclaredField(fieldName);
			field = loadField(formField, false);

		} catch (NoSuchFieldException e) {
			// don't care
		}
		
		try {
			// now grab the field from the model if it exists.
			modelField = findField(model, property);
			if (formField == null && modelField != null) {
				// need to create the field.
				field = getFieldType(property, modelField);
				if (field != null) {
					field.setName(property.getName());
					_fields.put(modelField.getName(), field);
				}
			}
		} catch (NoSuchFieldException e1) {
			// don't care
		}

		if (field == null) {
			// oops could not find the field.
			log.debug("Could not find field in ModelForm: " + fieldName);
			return null;
		}
		
		// process any annotations in the model
		if (modelField != null) {
			Annotation[] annotations = modelField.getAnnotations();
			for (Annotation annotation: annotations) {
				field.handleAnnotation(annotation);
				HibernateFormHelper.processHibernateAnnotation(annotation, field);
			}
		}

		// process any annotations in the form,  which will override model annotations.
		// this may cause funness due to default values,  ie if the model specifies required=false 
		// it will be overridden with the default value of true even
		// if required is not specified in the form's annotation.
		if (formField != null) {
			Annotation[] annotations = formField.getAnnotations();
			for (Annotation annotation: annotations) {
				field.handleAnnotation(annotation);
				HibernateFormHelper.processHibernateAnnotation(annotation, field);
			}
		}
 
		return field;
	}
	
	@Override
	protected void processModel() {		
		Configuration cfg = hibernateUtil.getConfiguration();		
		PersistentClass pc = cfg.getClassMapping(model.getName());

		if (pc == null) {
			// could not find hibernate configuration for this form,  fallback
			super.processModel();
			return;
		}
		
		if (getInclude() != null) {
			// only process the fields specified in include,  preserving order
			for(String fieldName : getInclude()) {
				try {
					if (!StringUtils.isBlank(fieldName)) {
						processField(pc.getProperty(fieldName), fieldName);
					}
				} catch (MappingException e) {
					log.error("Field not found :" + fieldName);
				}
			}
		} else {
			Class<?> clazz = model;
			while(clazz != null) {
				if (pc != null) {
					Iterator<Property> propIterator = pc.getPropertyIterator();
					while (propIterator.hasNext()) {
						Property property = propIterator.next();
						String fieldName = property.getName();
						if (exclude != null && exclude.contains(fieldName)) {
							if (log.isDebugEnabled()) log.debug("Excluding Field :" + fieldName);
						} else {
							processField(property, fieldName);
						}
					}
				}
				clazz = clazz.getSuperclass();
				if (clazz != null) pc = cfg.getClassMapping(clazz.getName());
			}
		}
	}
	
}
